﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Utility.Math;
using System;
using System.Reflection;
using System.Linq;
using Utility.Inspector;

namespace Utility.NodeGraphs
{
	[NodeEditor (typeof (Node))]
	public class NodeEditor
	{
		#region static
		static Dictionary<Type, Type> editorTypeDict;
		static Dictionary<Type, NodeEditor> editorDict = new Dictionary<Type, NodeEditor> ();

		static void CollectEditorTypes ()
		{
			Type t_nodeEditor = typeof (NodeEditor);
			editorTypeDict = (from assembly in AppDomain.CurrentDomain.GetAssemblies ()
							  from type in assembly.GetTypes ()
							  where type.Equals (t_nodeEditor) || type.IsSubclassOf (t_nodeEditor)
							  where !type.IsAbstract
							  let atts = type.GetCustomAttributes (typeof (NodeEditorAttribute), false)
							  where atts != null && atts.Length > 0
							  from obj in atts
							  let att = (NodeEditorAttribute)obj
							  group type by att.targetType)
							 .ToDictionary (group => group.Key, group => group.First ());
		}

		static NodeEditor CreateNodeEditor (Type nodeType)
		{
			if (editorTypeDict == null)
			{
				CollectEditorTypes ();
			}

			Type t_node = typeof (Node);
			Type editorType = null;
			while (nodeType.Equals (t_node) || nodeType.IsSubclassOf (t_node))
			{
				if (editorTypeDict.TryGetValue (nodeType, out editorType))
				{
					break;
				}
				else
				{
					nodeType = nodeType.BaseType;
				}
			}

			var editor = Activator.CreateInstance (editorType) as NodeEditor;
			if (editor == null)
			{
				Debug.LogWarning ($"Cannot create editor for {nodeType}");
			}
			return editor;
		}

		public static NodeEditor GetEditor (Type type)
		{
			NodeEditor editor = null;
			if (!editorDict.TryGetValue (type, out editor))
			{
				editor = CreateNodeEditor (type);
				editorDict[type] = editor;
			}
			return editor;
		}

		public static NodeEditor GetEditor (Node node)
		{
			return GetEditor (node.GetType ());
		}
		#endregion

		protected static NodeGraphEditorWindow GetWindow (NodeGraph graph) => NodeGraphEditorWindow.GetWindow (graph);

		public NodeEditor () { }

		public virtual Vector2 GetSize (Node node) => new Vector2 (120, 50);

		public Rect GetRect (Node node) => new Rect (node.position, GetSize (node));

		public virtual void DrawNode (Rect position, Node node, bool selected)
		{
			//node body
			DrawNodeBody (position, node);

			//ports
			DrawPorts (position, node);

			//window controls
			DrawDraggableRegion (GetDraggableRect (position, node), node);

			//block click to the background
			GUI.Button (position, "", GUIStyle.none);
		}

		public virtual Rect GetDraggableRect (Rect nodeRect, Node node)
		{
			return nodeRect;
		}

		public virtual void DrawNodeBody (Rect nodeRect, Node node)
		{
			GUI.Box (nodeRect, node.name, GUI.skin.FindStyle ("flow node 0"));
		}

		public virtual void DrawPorts (Rect nodeRect, Node node)
		{
			for (int i = 0; i < node.nodePorts.Count; i++)
			{
				var port = node.nodePorts[i];
				port.rect = nodeRect;
			}
		}

		public virtual void DrawConnection (NodePort sourcePort, NodePortOutput output)
		{
			Vector2 start = sourcePort.CanvasCenter;
			Vector2 end = output.TargetPort.CanvasCenter;

			Vector2 startTangent = start + output.outDirPoint;
			Vector2 endTangent = end + output.inDirPoint;

			Color color = Color.white;
			float width = 5;

			EditorGUIEX.DrawBezierCurve (start, end, startTangent, endTangent, color, width, true);
		}

		public virtual void DrawDraggableRegion (Rect draggableRect, Node node)
		{
			Event e = Event.current;
			if (e.button == 1)
			{
				//node menu
				if (GUI.Button (draggableRect, "", GUIStyle.none))
				{
					OpenNodeContextMenu (node);
				}
			}
			else
			{
				GUI.DragWindow (draggableRect);
			}
		}
		#region node context menu
		public void OpenNodeContextMenu (Node node)
		{
			GenericMenu menu = BuildNodeContextMenu (node);
			if (menu == null)
			{
				return;
			}

			var m = GUI.matrix;
			GUI.matrix = Matrix4x4.identity;
			menu.ShowAsContext ();
			GUI.matrix = m;
		}

		protected virtual GenericMenu BuildNodeContextMenu (Node node)
		{
			GenericMenu menu = new GenericMenu ();
			menu.AddItem (new GUIContent ("Connect"), false, () => GetWindow (node.graph).StartConnect (node.nodePorts[0]));
			AddMenuItem_StartConnection (menu, node.GetPort (0));
			AddMenuItem_RemoveAllConnections (menu, node);
			AddMenuItem_RemoveNode (menu, node);
			return menu;
		}

		#endregion
		#region port context menu
		public void OpenPortContextMenu (NodePort port)
		{
			GenericMenu menu = BuildPortContextMenu (port);
			if (menu == null)
			{
				return;
			}

			var m = GUI.matrix;
			GUI.matrix = Matrix4x4.identity;
			menu.ShowAsContext ();
			GUI.matrix = m;
		}

		protected virtual GenericMenu BuildPortContextMenu (NodePort port)
		{
			return null;
		}
		#endregion
		#region menu items
		//ports
		protected void AddMenuItem_StartConnection (GenericMenu menu, NodePort port, string text = "Connect")
		{
			if (port.ReachOutputLimit)
			{
				menu.AddDisabledItem (new GUIContent (text));
			}
			else
			{
				menu.AddItem (new GUIContent (text), false, () => GetWindow (port.Node.graph).StartConnect (port));
			}
		}

		protected void AddMenuItem_RemoveConnections (GenericMenu menu, NodePort port, string text = "Remove Connections")
		{
			menu.AddItem (new GUIContent (text), false, () => port.RemoveConnections ());
		}

		protected void AddMenuItem_RemovePort (GenericMenu menu, NodePort port, string text = "Remove Port")
		{
			if (port.CanBeRemoved)
			{
				menu.AddItem (new GUIContent (text), false, () => port.Node.RemovePort (port));
			}
			else
			{
				menu.AddDisabledItem (new GUIContent (text));
			}
		}

		protected void AddMenuItem_InsertPortAbove (GenericMenu menu, NodePort port, int inputLimit, int outputLimit, Vector2 dirPoint, string text = "Insert Port Above")
		{
			if (port.Node.CanAddPort ())
			{
				menu.AddItem (new GUIContent (text), false, () => port.Node.InsertNewPort (port.Index, inputLimit, outputLimit, dirPoint));
			}
			else
			{
				menu.AddDisabledItem (new GUIContent (text));
			}
		}

		protected void AddMenuItem_InsertPortBelow (GenericMenu menu, NodePort port, int inputLimit, int outputLimit, Vector2 dirPoint, string text = "Insert Port Below")
		{
			if (port.Node.CanAddPort ())
			{
				menu.AddItem (new GUIContent (text), false, () => port.Node.InsertNewPort (port.Index + 1, inputLimit, outputLimit, dirPoint));
			}
			else
			{
				menu.AddDisabledItem (new GUIContent (text));
			}
		}

		//nodes
		protected void AddMenuItem_RemoveNode (GenericMenu menu, Node node, string text = "Remove Node")
		{
			menu.AddItem (new GUIContent (text), false, () => node.graph.RemoveNode (node));
		}

		protected void AddMenuItem_RemoveAllConnections (GenericMenu menu, Node node, string text = "Remove All Connections")
		{
			menu.AddItem (new GUIContent (text), false, () => node.RemoveAllConnections ());
		}

		protected void AddMenuItem_AddPort (GenericMenu menu, Node node, int inputLimit, int outputLimit, Vector2 dirPoint, string text = "Add Port")
		{
			if (node.CanAddPort ())
			{
				menu.AddItem (new GUIContent (text), false, () => node.AddNewPort (inputLimit, outputLimit, dirPoint));
			}
			else
			{
				menu.AddDisabledItem (new GUIContent (text));
			}
		}
		#endregion
	}
}